Skip to content

MODEL-NEMOTRON-H W2: the non-gated relu² MoE expert — one GEMM, not a merged pair (#517) - #528

Draft
localai-bot wants to merge 1 commit into
mainfrom
row/MODEL-NEMOTRON-H-W2
Draft

MODEL-NEMOTRON-H W2: the non-gated relu² MoE expert — one GEMM, not a merged pair (#517)#528
localai-bot wants to merge 1 commit into
mainfrom
row/MODEL-NEMOTRON-H-W2

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Row: MODEL-TEXT-nemotron-h-nemotron-hfor-causal-lmW2 of
.agents/specs/nemotron-h-model.md.
Issue: #517. Base SHA: 9fd9e8f34408d5dd21d7f9385e96fc755708950b.

What

The non-gated relu² MoE expert arm. NemotronH's expert has no gate half —
ckpt_names=("up_proj", "down_proj", "") (nemotron_h.py:220 @ 555967922),
the empty third entry being the absent gate — so it is
up_proj -> relu² -> down_proj, not a merged pair.

Seam verdict

This is not a new merged pair and gets no MergedGemmGroup descriptor.
MergedGemmGroup describes N GEMMs sharing operand A collapsed into one
launch; with N == 1 there is nothing to merge and no launch to save, so an
arity-1 descriptor would name a fusion that does not exist.
MlpGateUpMethodBase is likewise a merged [2I,H] gate_up seam with no pair to
hold. The arm is the existing grouped projection plus the activation we did
not have — the shape the gated bf16 archs had before their pair was folded
(kMoeGroupedGemmBf16 + kMoeSiluMul). The reasoning is recorded next to the
seam it excludes, in merged_gemm.h. No parallel MoE path was added.

up   : kMoeGroupedGemmBf16  | kMoeGroupedGemmNvfp4Marlin (W4A16 g16)
act  : kMoeRelu2                 <- the only new kernel
down : kMoeGroupedGemmBf16  | kMoeGroupedGemmNvfp4Marlin (W4A16 g16)
comb : kMoeCombine(..., routed_scale)

vt::MoeRelu2 (OpId::kMoeRelu2, appended before kCount; CPU + CUDA)

Mirrors ReLUSquaredActivation (layers/activation.py:609-628) as the fused-MoE
path reaches it: activation_without_mul("relu2")MoEActivation.RELU2_NO_MUL
(layers/fused_moe/activation.py:33,98) → F.relu(input, inplace=True); torch.square(input, out=output). The dtype order is the mirrored part:
upstream's kernel (csrc/libtorch_stable/activation_kernels.cu:673-678) widens
to f32, clamps at zero in f32, squares in f32, rounds once on the store. No
new f32 buffer — the op reads and writes the caller's dtype; only the arithmetic
is f32.

routed_scaling_factor on the OUTPUT

apply_routed_scale_to_output=True (nemotron_h.py:234), so vt::MoeCombine
gained a trailing routed_scale (default 1.0f, every landed caller
byte-identical — proven by a memcmp test) multiplying the routed sum before
the shared term is added. That is moe_runner.py:389-406 (fused_output *= routed_scaling_factor, shared_output untouched) then :722-725
(shared_output + fused_output). Upstream forces the router's factor to
1.0 in exactly this case (layer.py:291-300) — the opposite polarity from
Laguna, which folds the same factor into the router weights by linearity
(laguna_ops.h:48).

group_size=16 NVFP4 — the spec's named risk: SUPPORTED, not emulated

MoeMarlinArgs already defaults to group_size=16 / mxfp4=false, and
cuda_moe_marlin.cu:7,115-129 consumes exactly that (group_blocks=1,
s_type=kFE4M3fn, num_groups = size_k / group_size); 32 is reachable only via
the MXFP4 branch. A test pins the default so a later widening cannot silently
re-point these experts.

Evidence

RED first — the new test failed to build:

error: 'MoeRelu2' is not a member of 'vt'
error: too many arguments to function 'void vt::MoeCombine(...)'

Green after — focused 10/10 cases, 71/71 assertions, Status: SUCCESS!;
clean-tree Release -Werror rebuild + ctest: 395/395 passed, 0 failed;
Debug arm (Release is NDEBUG) green on test_ops_moe_nongated_relu2,
test_ops_moe, test_ops_moe_router_grouped, test_ops_moe_grouped,
test_op_parity. scripts/agent-preflight.sh --staged: all gates green.

Mutations executed and caught (restored and re-proven green after each):

Mutation Result
relu(x) instead of relu(x)² 5 cases / 27 assertions RED
silu instead of relu² 5 cases / 35 assertions RED
square narrowed through bf16 before the store 2 cases / 20 assertions RED
routed scale dropped entirely 2 cases / 30 assertions RED
routed scale applied to the combined output (shared term too) 2 cases / 29 assertions RED
routed scale folded into the router logits 3 cases / 498 assertions RED in test_ops_moe_router_grouped
NVFP4 group_size default moved to 32 1 assertion RED

The bf16-narrowing mutation is only visible on the bf16-in / f32-out arm; a
bf16-out-only test absorbs it, which is why that arm is swept explicitly.

Owed, not claimed

This worktree has no GPU (nvcc absent), so the CUDA arms — kMoeRelu2 on
kCUDA and kMoeGroupedGemmNvfp4Marlin on the real g16 tensors — are
compiled-and-reviewed only and are recorded in the spec's W2 note as owed to a
GB10 run. W3/W4 still own wiring this into NemotronH itself (loader, model file).

🤖 Generated with Claude Code

…M, not a merged pair (#517)

Every grouped-MoE path in this tree is SwiGLU-shaped: a merged gate+up pair with
a silu(gate)*up epilogue. NemotronH's expert has no gate half at all --
`ckpt_names=("up_proj", "down_proj", "")` (nemotron_h.py:220 @ 555967922), the
empty third entry being the absent gate -- so the expert is

    h = up_proj(x); h = relu(h)^2; y = down_proj(h)

SEAM VERDICT: this is NOT a new merged pair and gets no `MergedGemmGroup`
descriptor. `MergedGemmGroup` describes N GEMMs SHARING operand A collapsed into
one launch; with N == 1 there is nothing to merge and no launch to save, so an
arity-1 descriptor would name a fusion that does not exist. `MlpGateUpMethodBase`
is likewise a merged [2I,H] gate_up seam with no pair to hold. The arm is
therefore the EXISTING grouped projection plus the activation we did not have --
exactly the shape the gated bf16 archs had before their pair was folded
(kMoeGroupedGemmBf16 + kMoeSiluMul). The reasoning is recorded next to the seam
it excludes, in merged_gemm.h. No parallel MoE path was added.

    up   : kMoeGroupedGemmBf16  | kMoeGroupedGemmNvfp4Marlin (W4A16 g16)
    act  : kMoeRelu2                 <- the only new kernel
    down : kMoeGroupedGemmBf16  | kMoeGroupedGemmNvfp4Marlin (W4A16 g16)
    comb : kMoeCombine(..., routed_scale)

vt::MoeRelu2 (OpId::kMoeRelu2, appended before kCount; CPU + CUDA) mirrors
ReLUSquaredActivation (layers/activation.py:609-628) as the fused-MoE path
reaches it: activation_without_mul("relu2") -> MoEActivation.RELU2_NO_MUL
(layers/fused_moe/activation.py:33,98) -> `F.relu(input, inplace=True);
torch.square(input, out=output)`. The DTYPE ORDER is the mirrored part, not an
implementation detail: upstream's kernel (csrc/libtorch_stable/
activation_kernels.cu:673-678) widens to f32, clamps at zero in f32, squares in
f32 and rounds ONCE on the store. No new f32 buffer is introduced -- the op
reads and writes the caller's dtype and only its arithmetic is f32.

routed_scaling_factor goes on the OUTPUT (apply_routed_scale_to_output=True,
nemotron_h.py:234), so vt::MoeCombine gained a trailing `routed_scale`
(default 1.0f -- every landed caller stays byte-identical, proven by a memcmp
test) that multiplies the routed sum BEFORE the shared term is added. That is
literally moe_runner.py:389-406 (`fused_output *= routed_scaling_factor`,
`shared_output` untouched) then :722-725 (`shared_output + fused_output`).
Upstream forces the ROUTER's factor to 1.0 in exactly this case
(layer.py:291-300), which is the opposite polarity from Laguna, which folds the
same factor into the router weights by linearity (laguna_ops.h:48).

group_size=16 NVFP4 -- the spec's named risk -- is SUPPORTED, not emulated:
MoeMarlinArgs already defaults to group_size=16 / mxfp4=false, and
cuda_moe_marlin.cu:7,115-129 consumes exactly that (group_blocks=1,
s_type=kFE4M3fn, num_groups=size_k/group_size); 32 is reachable only via the
MXFP4 branch. A test pins the default so a later widening cannot silently
re-point these experts.

RED first: the new test failed to build on `vt::MoeRelu2 is not a member of vt`
and `too many arguments to vt::MoeCombine`. Green after: focused 10/10 cases,
71/71 assertions, Status SUCCESS; clean-tree Release -Werror rebuild 395/395
ctest; Debug arm green on the MoE + op-parity suites (Release is NDEBUG).

Mutations executed and caught (restored and re-proven green after each):
relu instead of relu^2 (5 cases red), silu instead of relu^2 (5 red), the square
narrowed through bf16 before the store (2 red -- the bf16-in/f32-out arm is what
sees it; a bf16-out-only test absorbs it), routed scale dropped (2 red), routed
scale applied to the combined output including the shared term (2 red), routed
scale folded into the router LOGITS (3 cases / 498 assertions red in
test_ops_moe_router_grouped), NVFP4 group_size default moved to 32 (1 red).

OWED, not claimed: this worktree has no GPU (nvcc absent), so the CUDA arms --
kMoeRelu2 on kCUDA and kMoeGroupedGemmNvfp4Marlin on the real g16 tensors -- are
compiled-and-reviewed only. The spec's W2 note records them as owed to a GB10
run. W3/W4 (loader, model file) still own wiring this into NemotronH itself.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [Claude Code]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants